#include <string.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdio.h>

#include <psp2/kernel/processmgr.h>

#include "setting/setting.h"
#include "emu/emu.h"
#include "utils.h"
#include "config.h"
#include "file.h"
#include "lang.h"
#include "utils_image.h"

#define MICROS_PER_SECOND 1000000llu
#define SHOW_PLAYER_DURATION_MICROS (MICROS_PER_SECOND * 2)

#define MAX_VIDEO_TEXTURES 3

static int video_okay = 0, video_pause = 1;
static int video_display_need_update = 1;

static SceUID video_semaid = -1;
static GUI_Texture *video_textures[MAX_VIDEO_TEXTURES] = {0};
static int video_texture_index = 0;
static GUI_Texture *overlay_texture = NULL;
static GUI_Shader *video_shader = NULL;
static int video_overlay_select = -1;

static unsigned int video_width = 0, video_height = 0;
static float video_x = 0.0f, video_y = 0.0f;
static float video_x_scale = 1.0f, video_y_scale = 1.0f;
static float video_rotate_rad = 0.0f;

static unsigned int video_frames = 0;
static float video_fps = 0.0f;

static uint64_t micros_per_frame = 0;
static uint64_t last_frame_micros = 0;

static uint64_t last_fps_micros = 0;
static uint64_t show_player_micros = 0;

void Emu_PauseVideo()
{
    video_pause = 1;
    GUI_SetVblankWait(1);
    Emu_SignalVideoSema(); // 防止卡死
}

void Emu_ResumeVideo()
{
    video_pause = 0;
    GUI_SetVblankWait(0);
}

void Emu_RequestUpdateVideoDisplay()
{
    video_display_need_update = 1;
}

void Emu_ShowControllerPortToast()
{
    show_player_micros = sceKernelGetProcessTimeWide() + SHOW_PLAYER_DURATION_MICROS;
}

GUI_Texture *Emu_GetCurrentVideoTexture()
{
    return video_textures[video_texture_index];
}

GUI_Texture *Emu_GetNextVideoTexture()
{
    int next_texture_index = (video_texture_index + 1) % MAX_VIDEO_TEXTURES;
    GUI_Texture *next_texture = video_textures[next_texture_index];
    return next_texture;
}

static int converRotateCCWToCW(int rotate_ccw)
{
    if (rotate_ccw == 1)
        return TYPE_DISPLAY_ROTATE_CW_270;
    else if (rotate_ccw == 2)
        return TYPE_DISPLAY_ROTATE_CW_180;
    else if (rotate_ccw == 3)
        return TYPE_DISPLAY_ROTATE_CW_90;

    return TYPE_DISPLAY_ROTATE_DISABLE;
}

int Emu_GetVideoDisplayRotateCW() // Clockwise
{
    OverlayEntryData *overlay_data = NULL;
    if (graphics_overlay_list && graphics_config.overlay_select > 0)
    {
        LinkedListEntry *entry = LinkedListFindByNum(graphics_overlay_list, graphics_config.overlay_select - 1);
        overlay_data = (OverlayEntryData *)LinkedListGetEntryData(entry);
    }

    if (overlay_data && overlay_data->viewport_rotate)
        return *(overlay_data->viewport_rotate);
    else if (graphics_config.display_rotate == TYPE_DISPLAY_ROTATE_DEFAULT)
        return converRotateCCWToCW(core_display_rotate_ccw);
    else
        return graphics_config.display_rotate;
}

void Emu_GetVideoBaseDimensions(uint32_t *width, uint32_t *height)
{
    if (!width || !height)
        return;

    OverlayEntryData *overlay_data = NULL;
    uint32_t base_width = core_system_av_info.geometry.base_width;
    uint32_t base_height = core_system_av_info.geometry.base_height;
    float aspect_ratio = 0;

    if (graphics_overlay_list && graphics_config.overlay_select > 0)
    {
        LinkedListEntry *entry = LinkedListFindByNum(graphics_overlay_list, graphics_config.overlay_select - 1);
        overlay_data = (OverlayEntryData *)LinkedListGetEntryData(entry);
    }

    if (overlay_data && overlay_data->viewport_width && overlay_data->viewport_height)
    {
        int rotate_cw = Emu_GetVideoDisplayRotateCW();
        if (rotate_cw == TYPE_DISPLAY_ROTATE_CW_90 || rotate_cw == TYPE_DISPLAY_ROTATE_CW_270)
            aspect_ratio = (float)*(overlay_data->viewport_height) / *(overlay_data->viewport_width);
        else
            aspect_ratio = (float)*(overlay_data->viewport_width) / *(overlay_data->viewport_height);
    }
    else if (graphics_config.aspect_ratio == TYPE_DISPLAY_RATIO_BY_GAME_RESOLUTION)
    {
        *width = base_width;
        *height = base_height;
        return;
    }
    else if (graphics_config.aspect_ratio == TYPE_DISPLAY_RATIO_DEFAULT)
    {
        aspect_ratio = core_system_av_info.geometry.aspect_ratio;
    }
    else if (graphics_config.aspect_ratio == TYPE_DISPLAY_RATIO_BY_DEVICE_SCREEN)
    {
        aspect_ratio = (float)GUI_SCREEN_WIDTH / GUI_SCREEN_HEIGHT;
    }
    else if (graphics_config.aspect_ratio == TYPE_DISPLAY_RATIO_8_7)
    {
        aspect_ratio = 8.f / 7.f;
    }
    else if (graphics_config.aspect_ratio == TYPE_DISPLAY_RATIO_4_3)
    {
        aspect_ratio = 4.f / 3.f;
    }
    else if (graphics_config.aspect_ratio == TYPE_DISPLAY_RATIO_3_2)
    {
        aspect_ratio = 3.f / 2.f;
    }
    else if (graphics_config.aspect_ratio == TYPE_DISPLAY_RATIO_16_9)
    {
        aspect_ratio = 16.f / 9.f;
    }

    if (aspect_ratio == 0)
        aspect_ratio = (float)base_width / base_height;

    *height = base_height;
    *width = *height * aspect_ratio;
}

void Emu_GetVideoDisplayDimensions(uint32_t *width, uint32_t *height)
{
    OverlayEntryData *overlay_data = NULL;
    if (graphics_overlay_list && graphics_config.overlay_select > 0)
    {
        LinkedListEntry *entry = LinkedListFindByNum(graphics_overlay_list, graphics_config.overlay_select - 1);
        overlay_data = (OverlayEntryData *)LinkedListGetEntryData(entry);
    }

    if (overlay_data && overlay_data->viewport_width && overlay_data->viewport_height)
    {
        *width = *(overlay_data->viewport_width);
        *height = *(overlay_data->viewport_height);
        return;
    }

    uint32_t base_width = 0, base_height = 0;
    uint32_t new_width = 0, new_height = 0;
    float aspect_ratio;

    Emu_GetVideoBaseDimensions(&base_width, &base_height);

    int rotate_cw = Emu_GetVideoDisplayRotateCW();
    if (rotate_cw == TYPE_DISPLAY_ROTATE_CW_90 || rotate_cw == TYPE_DISPLAY_ROTATE_CW_270)
    {
        uint32_t tmp_width = base_width;
        base_width = base_height;
        base_height = tmp_width;
    }

    aspect_ratio = (float)base_width / base_height;

    if (graphics_config.display_size == TYPE_DISPLAY_SIZE_2X)
    { // 2倍大小
        new_width = base_width * 2;
        new_height = base_height * 2;
    }
    else if (graphics_config.display_size == TYPE_DISPLAY_SIZE_3X)
    { // 3倍大小
        new_width = base_width * 3;
        new_height = base_height * 3;
    }
    else if (graphics_config.display_size == TYPE_DISPLAY_SIZE_FULL)
    { // 铺满屏幕
        new_height = GUI_SCREEN_HEIGHT;
        new_width = new_height * aspect_ratio;
    }
    else
    { // 1倍大小
        new_width = base_width;
        new_height = base_height;
    }

    // 检测越界
    if (new_width > GUI_SCREEN_WIDTH)
    {
        new_width = GUI_SCREEN_WIDTH;
        new_height = new_width / aspect_ratio;
    }
    if (new_height > GUI_SCREEN_HEIGHT)
    {
        new_height = GUI_SCREEN_HEIGHT;
        new_width = new_height * aspect_ratio;
    }

    *width = new_width;
    *height = new_height;
}

void *Emu_GetVideoScreenshotData(uint32_t *width, uint32_t *height, uint32_t *size, int rotate_cw)
{
    GUI_Texture *video_texture = video_textures[video_texture_index];

    if (!video_okay || !video_texture)
        return NULL;

    uint32_t dst_width = *width;
    uint32_t dst_height = *height;
    float aspect_ratio = (float)*width / *height;

    // Fix above
    if (dst_width > GUI_SCREEN_WIDTH)
    {
        dst_width = GUI_SCREEN_WIDTH;
        dst_height = dst_width / aspect_ratio;
    }
    if (dst_height > GUI_SCREEN_HEIGHT)
    {
        dst_height = GUI_SCREEN_HEIGHT;
        dst_width = dst_height * aspect_ratio;
    }
    dst_width = (dst_width + 7) & ~7;
    if (dst_width != *width)
        dst_height = dst_width / aspect_ratio;

    uint32_t dst_size = dst_height * dst_width * 4;
    void *dst_data = (uint32_t *)malloc(dst_size);
    if (!dst_data)
    {
        printf("Emu_GetVideoScreenshotData failed: alloc screenshot data failed!\n");
        return NULL;
    }

    GUI_LockDrawMutex();
    void *src_data = GUI_GetTextureDatap(video_texture);
    int src_format = GUI_GetTextureFormat(video_texture);
    uint32_t src_width = GUI_GetTextureWidth(video_texture);
    uint32_t src_height = GUI_GetTextureHeight(video_texture);
    uint32_t src_pitch = GUI_GetTextureStride(video_texture) / GUI_PixelFormatToBytespp(src_format);

    ConvertImageToABGR8888(dst_data, dst_width, dst_height, dst_width,
                           src_data, src_width, src_height, src_pitch,
                           src_format, rotate_cw);
    GUI_UnlockDrawMutex();

    *width = dst_width;
    *height = dst_height;
    *size = dst_size;
    // printf("dst_width: %d, dst_height: %d\n", dst_width, dst_height);

    return dst_data;
}

int Emu_SaveVideoScreenshot(char *path)
{
    int ret = 0;

    char parent_path[MAX_PATH_LENGTH];
    MakeParentDir(parent_path, path, MAX_PATH_LENGTH);
    CreateFolder(parent_path);

    uint32_t base_width, base_height;
    Emu_GetVideoBaseDimensions(&base_width, &base_height);

    uint32_t screenshot_width, screenshot_height;
    int rotate_cw = Emu_GetVideoDisplayRotateCW();

    if (rotate_cw == TYPE_DISPLAY_ROTATE_CW_90 || rotate_cw == TYPE_DISPLAY_ROTATE_CW_270)
    {
        screenshot_width = base_height;
        screenshot_height = base_width;
    }
    else
    {
        screenshot_width = base_width;
        screenshot_height = base_height;
    }

    uint32_t screenshot_size = 0;
    void *screenshot_buf = Emu_GetVideoScreenshotData(&screenshot_width, &screenshot_height, &screenshot_size, rotate_cw);
    if (!screenshot_buf)
        return -1;

    ret = WritePngFile(path, (unsigned char *)screenshot_buf, screenshot_width, screenshot_height, 8);
    free(screenshot_buf);
    return ret;
}

static void destroyVideoTexture()
{
    int i;
    for (i = 0; i < MAX_VIDEO_TEXTURES; i++)
    {
        if (video_textures[i])
        {
            GUI_DestroyTexture(video_textures[i]);
            video_textures[i] = NULL;
        }
    }
}

static GUI_Texture *createVideoTexture(int width, int height)
{
    GUI_Texture *video_texture = NULL;

    destroyVideoTexture();

    video_width = width;
    video_height = height;

    int i;
    for (i = 0; i < MAX_VIDEO_TEXTURES; i++)
    {
        video_textures[i] = GUI_CreateTextureFormat(width, height, core_video_pixel_format);
    }

    video_texture_index = 0;
    video_texture = video_textures[video_texture_index];

    if (!video_texture)
    {
        APP_LOG("[VIDEO] create video texture failed\n");
        return NULL;
    }

    return video_texture;
}

static void destroyOverlayTexture()
{
    if (overlay_texture)
    {
        GUI_DestroyTexture(overlay_texture);
        overlay_texture = NULL;
    }
}

static GUI_Texture *createOverlayTexture()
{
    Setting_WaitOverlayInitEnd();
    destroyOverlayTexture();

    video_overlay_select = graphics_config.overlay_select;

    OverlayEntryData *overlay_data = NULL;
    if (graphics_overlay_list && graphics_config.overlay_select > 0)
    {
        LinkedListEntry *entry = LinkedListFindByNum(graphics_overlay_list, graphics_config.overlay_select - 1);
        overlay_data = (OverlayEntryData *)LinkedListGetEntryData(entry);
    }

    if (!overlay_data || !overlay_data->image_name) // No found
        return NULL;

    char path[MAX_PATH_LENGTH];

    // Try load image from app data overlays dir
    snprintf(path, MAX_PATH_LENGTH, "%s/%s/%s", APP_DATA_DIR, OVERLAYS_DIR_NAME, overlay_data->image_name);
    overlay_texture = GUI_LoadPNGFile(path);

    // Try load image from private assets dir
    if (!overlay_texture && private_assets_dir)
    {
        snprintf(path, MAX_PATH_LENGTH, "%s/%s/%s", private_assets_dir, OVERLAYS_DIR_NAME, overlay_data->image_name);
        overlay_texture = GUI_LoadPNGFile(path);
    }

    // Try load image from public assets dir
    if (!overlay_texture && public_assets_dir)
    {
        snprintf(path, MAX_PATH_LENGTH, "%s/%s/%s", public_assets_dir, OVERLAYS_DIR_NAME, overlay_data->image_name);
        overlay_texture = GUI_LoadPNGFile(path);
    }

    if (!overlay_texture)
    {
        APP_LOG("[VIDEO] create overlay texture failed\n");
        return NULL;
    }

    return overlay_texture;
}

static int updateVideoDisplay()
{
    GUI_Texture *video_texture = video_textures[video_texture_index];
    OverlayEntryData *overlay_data = NULL;
    uint32_t width = 0, height = 0;

    if (!video_okay)
        return -1;

    // Overlay texture
    if (video_overlay_select != graphics_config.overlay_select)
        createOverlayTexture();

    if (!video_texture)
        return -1;

    // Overlay config
    if (graphics_overlay_list && graphics_config.overlay_select > 0)
    {
        LinkedListEntry *entry = LinkedListFindByNum(graphics_overlay_list, graphics_config.overlay_select - 1);
        overlay_data = (OverlayEntryData *)LinkedListGetEntryData(entry);
    }

    // Rotate
    int rotate_cw = Emu_GetVideoDisplayRotateCW();
    float radian = 90 * 0.0174532925f;
    video_rotate_rad = rotate_cw * radian;

    // Width & height
    Emu_GetVideoDisplayDimensions(&width, &height);

    // Coord
    video_x = GUI_SCREEN_HALF_WIDTH;
    video_y = GUI_SCREEN_HALF_HEIGHT;
    if (overlay_data)
    {
        if (overlay_data->viewport_x)
            video_x = *(overlay_data->viewport_x) + width / 2;
        if (overlay_data->viewport_y)
            video_y = *(overlay_data->viewport_y) + height / 2;
    }

    // Scale
    if (rotate_cw == TYPE_DISPLAY_ROTATE_CW_90 || rotate_cw == TYPE_DISPLAY_ROTATE_CW_270)
    {
        video_x_scale = (float)height / video_width;
        video_y_scale = (float)width / video_height;
    }
    else
    {
        video_x_scale = (float)width / video_width;
        video_y_scale = (float)height / video_height;
    }

    // Shader
    switch (graphics_config.graphics_shader)
    {
    case TYPE_GRAPHICS_SHADER_LCD3X:
        video_shader = lcd3x_shader;
        break;
    case TYPE_GRAPHICS_SHADER_SHARP_BILINEAR_SIMPLE:
        video_shader = sharp_bilinear_simple_shader;
        break;
    case TYPE_GRAPHICS_SHADER_SHARP_BILINEAR:
        video_shader = sharp_bilinear_shader;
        break;
    case TYPE_GRAPHICS_SHADER_ADVANCED_AA:
        video_shader = advanced_aa_shader;
        break;
    default:
        video_shader = NULL;
        break;
    }

    // Filters
    int i;
    for (i = 0; i < MAX_VIDEO_TEXTURES; i++)
    {
        if (video_textures[i])
        {
            if (graphics_config.graphics_smooth)
                GUI_SetTextureFilter(video_textures[i], GUI_TEXTURE_FILTER_LINEAR, GUI_TEXTURE_FILTER_LINEAR);
            else
                GUI_SetTextureFilter(video_textures[i], GUI_TEXTURE_FILTER_POINT, GUI_TEXTURE_FILTER_POINT);
        }
    }

    return 0;
}

static void refreshFps()
{
    uint64_t cur_micros = sceKernelGetProcessTimeWide();
    uint64_t interval_micros = cur_micros - last_fps_micros;
    if (interval_micros > 1000000)
    {
        last_fps_micros = cur_micros;
        video_fps = (video_frames / (double)interval_micros) * 1000000.0f;
        video_frames = 0;
    }
    video_frames++;
}

static void checkFrameDelay()
{
    uint64_t cur_micros = sceKernelGetProcessTimeWide();
    uint64_t interval_micros = cur_micros - last_frame_micros;
    if (interval_micros < micros_per_frame)
    {
        uint64_t delay_micros = micros_per_frame - interval_micros;
        sceKernelDelayThread(delay_micros);
        last_frame_micros = cur_micros + delay_micros;
    }
    else
    {
        last_frame_micros = cur_micros;
    }
}

void Emu_BeforeDrawVideo()
{
    if (!video_okay)
        return;

    GUI_LockDrawMutex();

    if (video_display_need_update)
    {
        updateVideoDisplay();
        video_display_need_update = 0;
    }

    GUI_UnlockDrawMutex();
}

static void drawWidgets()
{
    if (graphics_config.show_fps)
    {
        refreshFps();
        GUI_DrawTextf(0.0f, 0.0f, COLOR_WHITE, "FPS: %.2f", video_fps);
    }

    if (Emu_GetRunSpeed() != 1.0f)
    {
        char fps_scale_string[12];
        snprintf(fps_scale_string, 12, "%.2fX", Emu_GetRunSpeed());
        GUI_DrawText(GUI_SCREEN_WIDTH - GUI_GetTextWidth(fps_scale_string), 0.0f, COLOR_WHITE, fps_scale_string);
    }

    if (show_player_micros > 0)
    {
        uint64_t cur_micros = sceKernelGetProcessTimeWide();
        if (cur_micros < show_player_micros)
            GUI_DrawTextf(0.0f, GUI_SCREEN_HEIGHT - GUI_GetLineHeight(), COLOR_WHITE, "%s: %dP", cur_lang[LANG_CONTROLLER_PORT], control_config.controller_port + 1);
        else
            show_player_micros = 0;
    }
}

void Emu_DrawVideo()
{
    if (!video_okay)
        return;

    if (overlay_texture && graphics_config.overlay_mode == TYPE_GRAPHICS_OVERLAY_MODE_BACKGROUND)
        GUI_DrawTexture(overlay_texture, 0.0f, 0.0f);

    GUI_Texture *video_texture = video_textures[video_texture_index];
    if (video_texture)
    {
        if (video_shader)
            GUI_DrawTextureShaderPartScalRotate(video_texture, video_shader, video_x, video_y, 0, 0, video_width, video_height, video_x_scale, video_y_scale, video_rotate_rad);
        else
            GUI_DrawTexturePartScaleRotate(video_texture, video_x, video_y, 0, 0, video_width, video_height, video_x_scale, video_y_scale, video_rotate_rad);
    }

    if (overlay_texture && graphics_config.overlay_mode == TYPE_GRAPHICS_OVERLAY_MODE_OVERLAY)
        GUI_DrawTexture(overlay_texture, 0.0f, 0.0f);

    if (Emu_IsGameRunning())
        drawWidgets();
}

void Retro_VideoRefreshCallback(const void *data, unsigned width, unsigned height, size_t pitch)
{
    if (!video_okay)
        return;

    if (video_pause)
        goto SIGNAL_EXIT;

    if (!data || pitch == 0)
        goto NEXT_FRAME;

    int next_texture_index = (video_texture_index + 1) % MAX_VIDEO_TEXTURES;
    GUI_Texture *next_texture = video_textures[next_texture_index];

    if (next_texture && GUI_GetTextureDatap(next_texture) == data)
        goto SET_TEXTURE;

    if (video_width != width || video_height != height || !next_texture ||
        GUI_GetTextureFormat(next_texture) != core_video_pixel_format)
    {
        GUI_LockDrawMutex();
        createVideoTexture(width, height);
        updateVideoDisplay();
        GUI_UnlockDrawMutex();
        next_texture_index = video_texture_index;
        next_texture = video_textures[next_texture_index];
    }

    if (next_texture)
    {
        const uint8_t *in_data = (const uint8_t *)data;
        uint8_t *out_data = (uint8_t *)GUI_GetTextureDatap(next_texture);
        unsigned int in_pitch = pitch;
        unsigned int out_pitch = GUI_GetTextureStride(next_texture);

        int i;
        for (i = 0; i < height; i++)
        {
            memcpy(out_data, in_data, in_pitch);
            in_data += in_pitch;
            out_data += out_pitch;
        }
    }

SET_TEXTURE:
    GUI_LockDrawMutex();
    video_texture_index = next_texture_index;
    GUI_UnlockDrawMutex();

NEXT_FRAME:
    checkFrameDelay();

SIGNAL_EXIT:
    Emu_SignalVideoSema();
}

int Emu_InitVideo()
{
    APP_LOG("[VIDEO] Video init...\n");

    video_okay = 0;
    video_pause = 1;

    if (!createVideoTexture(core_system_av_info.geometry.base_width, core_system_av_info.geometry.base_height))
        return -1;

    createOverlayTexture();
    video_semaid = sceKernelCreateSema("emu_video_sema", 0, 0, 1, NULL);

    if (control_config.controller_port != 0)
        Emu_ShowControllerPortToast();

    APP_LOG("[VIDEO] max width: %d, max height: %d\n", core_system_av_info.geometry.max_width, core_system_av_info.geometry.max_height);
    APP_LOG("[VIDEO] base width: %d, base height: %d\n", core_system_av_info.geometry.base_width, core_system_av_info.geometry.base_height);
    APP_LOG("[VIDEO] FPS: %.2f\n", core_system_av_info.timing.fps);
    APP_LOG("[VIDEO] Video init OK!\n");

    video_okay = 1;
    GUI_LockDrawMutex();
    updateVideoDisplay();
    GUI_UnlockDrawMutex();

    return 0;
}

int Emu_DeinitVideo()
{
    APP_LOG("[VIDEO] Video deinit...\n");

    video_okay = 0;
    video_pause = 1;

    destroyVideoTexture();
    destroyOverlayTexture();
    if (video_semaid >= 0)
    {
        sceKernelDeleteSema(video_semaid);
        video_semaid = -1;
    }

    GUI_SetVblankWait(1);

    APP_LOG("[VIDEO] Video deinit OK!\n");
    return 0;
}

int Emu_SignalVideoSema()
{
    return sceKernelSignalSema(video_semaid, 1);
}

int Emu_WaitVideoSema()
{
    return sceKernelWaitSema(video_semaid, 1, NULL);
}

void Emu_SetMicrosPerFrame(uint64_t micros)
{
    micros_per_frame = micros;
}

uint64_t Emu_GetMicrosPerFrame()
{
    return micros_per_frame;
}
